package services;

import gestures.Gesture;

import java.awt.geom.Point2D;
import java.lang.reflect.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Map.Entry;

import org.w3c.dom.*;

/**
 * Handles incoming xml documents of gestures and converts them to constructed
 * gesture objects
 * 
 * @author PDAY (The MITRE Corporation)
 * 
 */
public final class GestureParser {

	private static SimpleDateFormat GESTURE_TIME_FORMAT = new SimpleDateFormat(
			"MM/dd/yyyy HH:mm:ss");

	// Gesture node relations in the XML document
	private static String GESTURE_NODE = "gesture";
	private static String GESTURE_ID_ATTR = "id";
	private static String GESTURE_INDEX_ATTR = "index";
	private static String GESTURE_TYPE_ATTR = "type";

	private static String GESTURE_TIME_ATTR = "time";

	private static String COORD_LIST_NODE = "coordinates";

	private static String COORD_NODE = "coordinate";
	private static String COORD_TYPE_ATTR = "type";
	private static String COORD_TYPE_CENTER = "CENTER";
	private static String COORD_TYPE_CONTACT = "CONTACT";
	private static String COORD_X_ATTR = "x";
	private static String COORD_Y_ATTR = "y";

	private static int INIT_GEST_PARAM_CT = 3;

	/**
	 * Parse gestures from a given XML Document.
	 * 
	 * @param doc
	 *            Properly structured XML document with gesture information
	 * @return Array of gesture objects
	 */
	public static Gesture[] parseGestures(Document doc) {

		List<Gesture> retGestures = new ArrayList<Gesture>();
		try {
			// Get all gesture nodes
			NodeList gestureList = doc.getElementsByTagName(GESTURE_NODE);

			for (int i = 0; i < gestureList.getLength(); i++) {
				Node node = gestureList.item(i);
				if (node.getNodeType() == Node.ELEMENT_NODE) {
					Element elem = (Element) node;

					retGestures.add(parseGesture(elem));
				}
			}

		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (ParseException e) {
			e.printStackTrace();
		}
		// Convert the list to an array and return
		Gesture[] gestArr = new Gesture[retGestures.size()];
		return retGestures.toArray(gestArr);
	}

	private static Gesture parseGesture(Element elem)
			throws InstantiationException, ParseException {
		Gesture currGesture = null;

		// Parse ID
		int id = Integer.parseInt(elem.getAttribute(GESTURE_ID_ATTR));

		// Parse the gesture type
		String gestType = elem.getAttribute(GESTURE_TYPE_ATTR);

		// Parse the time
		Calendar timeStamp = parseDatetimeString(
				elem.getAttribute(GESTURE_TIME_ATTR), GESTURE_TIME_FORMAT);

		// Parse the gesture coordinates
		NodeList coordNodes = elem.getElementsByTagName(COORD_LIST_NODE);
		List<Point2D.Float> coords = new ArrayList<Point2D.Float>();

		Point2D.Float gestCenter = null;
		Element coordListElem;
		for (int i = 0; i < coordNodes.getLength(); i++) {
			if (coordNodes.item(i).getNodeType() == Node.ELEMENT_NODE) {
				coordListElem = (Element) coordNodes.item(i);
				coords.addAll(Arrays.asList(getCoords(coordListElem)));
				gestCenter = getGestCenter(coordListElem);
			}
		}
		Point2D.Float[] currentCoords = new Point2D.Float[coords.size()];
		currentCoords = coords.toArray(currentCoords);

		// Note gestType must be full defined path (i.e.
		// gestures.tapGestures.TapGesture)
		currGesture = initGesture(id, gestType, timeStamp, currentCoords);
		if (currGesture != null) {
			// Set gesture center
			currGesture.setGestureCenter(gestCenter);
			// Get the gesture index
			String gestIndexStr = elem.getAttribute(GESTURE_INDEX_ATTR);
			int index;
			if (!gestIndexStr.equals("")) {
				index = Integer.parseInt(gestIndexStr);
				currGesture.setIndex(index);
			}

			return currGesture;
		} else
			throw new InstantiationException("Error creating initial gesture, of type:" + gestType);
	}

	private static Point2D.Float getGestCenter(Element coordsElem) {
		// Get the coordinate's children (individual
		// coordinates)
		NodeList coords = coordsElem.getElementsByTagName(COORD_NODE);
		for (int i = 0; i < coords.getLength(); i++) {
			Node coordNode = coords.item(i);
			if (coordNode.getNodeType() == Node.ELEMENT_NODE) {
				Element coordElem = (Element) coordNode;

				String coordType = coordElem.getAttribute(COORD_TYPE_ATTR);
				if (coordType != null && coordType.equalsIgnoreCase(COORD_TYPE_CENTER)) {
					float gestureX = Float.parseFloat(coordElem
							.getAttribute(COORD_X_ATTR));
					float gestureY = Float.parseFloat(coordElem
							.getAttribute(COORD_Y_ATTR));
					return new Point2D.Float(gestureX, gestureY);
				}
			}

		}
		return null;
	}

	/**
	 * Get coordinates for a specific nodelist
	 * 
	 * @param coordParentNode
	 *            Parent node containing a list of coodinate elements
	 * @return Array of converted Point2D objects
	 */
	private static Point2D.Float[] getCoords(Element coordsElem) {
		List<Point2D.Float> coordList = new ArrayList<Point2D.Float>();

		// Get the coordinate's children (individual
		// coordinates)
		NodeList coords = coordsElem.getElementsByTagName(COORD_NODE);
		for (int i = 0; i < coords.getLength(); i++) {
			Node coordNode = coords.item(i);
			if (coordNode.getNodeType() == Node.ELEMENT_NODE) {
				Element coordElem = (Element) coordNode;

				String coordType = coordElem.getAttribute(COORD_TYPE_ATTR);
				if (coordType != null && coordType.equalsIgnoreCase(COORD_TYPE_CONTACT)) {
					float gestureX = Float.parseFloat(coordElem
							.getAttribute(COORD_X_ATTR));
					float gestureY = Float.parseFloat(coordElem
							.getAttribute(COORD_Y_ATTR));
					coordList.add(new Point2D.Float(gestureX, gestureY));
				}
			}

		}
		// Convert list to array
		Point2D.Float[] retArr = new Point2D.Float[coordList.size()];
		return coordList.toArray(retArr);
	}

	/**
	 * Parse a datetime string into a Calendar object
	 * 
	 * @param value
	 *            String to convert
	 * @param format
	 *            Format datetime is in
	 * @return Properly converted datetime object
	 * @throws ParseException
	 *             If object is not a valid datetime or does not meet format
	 *             provider's format
	 */
	private static Calendar parseDatetimeString(String value,
			SimpleDateFormat format) throws ParseException {

		Date d = format.parse(value);
		Calendar timeStamp = Calendar.getInstance();
		timeStamp.setTime(d);

		return timeStamp;
	}

	/**
	 * Determine a gesture's type and create an instance
	 * 
	 * @param gestType
	 *            Gesture type to create (i.e. gestures.tapGestures.DoubleTap
	 * @param timeStamp
	 *            Time of this gesture
	 * @param coords
	 *            Coordinates for this gesture
	 * @return Instantiated subclass of Gesture
	 */
	private static Gesture initGesture(int id, String gestType,
			Calendar timeStamp, Point2D.Float[] coords) {
		Gesture retGesture = null;

		try {
			// determine gesture type
			Class<?> gestClass = null;

			Iterator<Entry<Class<?>, String>> nameIterator = GestureClassMap.GESTURE_CLASS_MAP
					.entrySet().iterator();
			while (nameIterator.hasNext()) {
				Entry<Class<?>, String> entry = nameIterator.next();
				if (entry.getValue().equals(gestType))
					gestClass = entry.getKey();
			}

			if (gestClass != null) {
				// Find proper constructor
				Constructor<?>[] gestConstructors = gestClass.getConstructors();
				for (Constructor<?> gestConstructor : gestConstructors) {
					if (gestConstructor.getParameterTypes().length == INIT_GEST_PARAM_CT) {
						retGesture = (Gesture) gestConstructor
								.newInstance(new Object[] { id, timeStamp,
										coords });
						break;
					}
				}
			}
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
		return retGesture;
	}
}
